#pragma once

// GCOVR_EXCL_START

#include "Device.h"
#include "MacAddress.h"
#include "SystemUtils.h"
#include "Packet.h"
#include <array>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>

/// @file

// forward declaration of PF_RING structs
struct __pfring;
typedef struct __pfring pfring;

/// @namespace pcpp
/// @brief The main namespace for the PcapPlusPlus lib
namespace pcpp
{

	class PfRingDevice;

	typedef void (*OnPfRingPacketsArriveCallback)(RawPacket* packets, uint32_t numOfPackets, uint8_t threadId,
	                                              PfRingDevice* device, void* userCookie);

	/// @class PfRingDevice
	/// A class representing a PF_RING port
	class PfRingDevice : public IFilterableDevice
	{
		friend class PfRingDeviceList;

	private:
		struct CoreConfiguration
		{
			std::thread RxThread;
			pfring* Channel;
			bool IsInUse;
			bool IsAffinitySet;

			CoreConfiguration();
			void clear();
		};

		class StartupBlock
		{
		public:
			void notifyStartup();
			void waitForStartup();

		private:
			std::mutex m_Mutex;
			std::condition_variable m_Cv;
			bool m_Ready = false;
		};

		bool m_DeviceOpened = false;

		std::vector<pfring*> m_PfRingDescriptors;
		std::string m_DeviceName;
		int m_InterfaceIndex;
		MacAddress m_MacAddress;
		int m_DeviceMTU;
		std::array<CoreConfiguration, MAX_NUM_OF_CORES> m_CoreConfiguration;
		std::atomic<bool> m_StopThread;
		OnPfRingPacketsArriveCallback m_OnPacketsArriveCallback;
		void* m_OnPacketsArriveUserCookie;
		bool m_ReentrantMode;
		bool m_HwClockEnabled;
		bool m_IsFilterCurrentlySet;

		PfRingDevice(const char* deviceName);

		bool initCoreConfigurationByCoreMask(CoreMask coreMask);
		void captureThreadMain(std::shared_ptr<StartupBlock> startupBlock);

		int openSingleRxChannel(const char* deviceName, pfring*& ring);
		/// Closes all opened RX channels and clears the opened channels list (m_PfRingDescriptors)
		void closeAllRxChannels();

		bool getIsHwClockEnable()
		{
			setPfRingDeviceAttributes();
			return m_HwClockEnabled;
		}
		bool setPfRingDeviceClock(pfring* ring);

		void clearCoreConfiguration();
		size_t getCoresInUseCount() const;

		void setPfRingDeviceAttributes();

		bool sendData(const uint8_t* packetData, int packetDataLength, bool flushTxQueues);

	public:
		/// An enum representing the type of packet distribution between different RX channels
		enum ChannelDistribution
		{
			/// Packets are distributed between channels in a round-robin manner
			RoundRobin,
			/// Packets are distributed between channels per flow (each flow goes for different channel)
			PerFlow
		};

		/// @struct PfRingStats
		/// A container for PfRingDevice statistics
		struct PfRingStats
		{
			/// Number of packets received
			uint64_t recv;
			/// Number of packets dropped
			uint64_t drop;
		};

		/// A destructor for PfRingDevice class
		~PfRingDevice();

		/// Get the MAC address of the current device
		/// @return The MAC address of the current device
		MacAddress getMacAddress()
		{
			setPfRingDeviceAttributes();
			return m_MacAddress;
		}

		/// Get PF_RING interface index of the current device
		/// @return PF_RING interface index of the current device
		int getInterfaceIndex()
		{
			setPfRingDeviceAttributes();
			return m_InterfaceIndex;
		}

		/// Get MTU of the current device
		/// @return Upon success return the device MTU, 0 otherwise
		int getMtu()
		{
			setPfRingDeviceAttributes();
			return m_DeviceMTU;
		}

		/// Return true if device supports hardware timestamping. If it does, this feature will be automatically set
		/// for this device. You can read more about this in PF_RING documentation
		/// @return True if device supports hardware timestamping, false otherwise
		bool isHwClockEnabledForDevice()
		{
			setPfRingDeviceAttributes();
			return m_HwClockEnabled;
		}

		/// Gets the interface name (e.g eth0, eth1, etc.)
		/// @return The interface name
		std::string getDeviceName() const
		{
			return m_DeviceName;
		}

		/// Start single-threaded capturing with callback. Works with open() or openSingleRxChannel().
		/// @param[in] onPacketsArrive A callback to call whenever a packet arrives
		/// @param[in] onPacketsArriveUserCookie A cookie that will be delivered to onPacketsArrive callback on every
		/// packet
		/// @return True if this action succeeds, false otherwise
		bool startCaptureSingleThread(OnPfRingPacketsArriveCallback onPacketsArrive, void* onPacketsArriveUserCookie);

		/// Start multi-threaded (multi-core) capturing with callback. Works with openMultiRxChannels().
		/// This method will return an error if the number of opened channels is different than the number of
		/// threads/cores requested
		/// @param[in] onPacketsArrive A callback to call whenever a packet arrives
		/// @param[in] onPacketsArriveUserCookie A cookie that will be delivered to onPacketsArrive callback on every
		/// packet
		/// @param[in] coreMask The cores to be used as mask. For example:
		/// @return True if this action succeeds, false otherwise
		bool startCaptureMultiThread(OnPfRingPacketsArriveCallback onPacketsArrive, void* onPacketsArriveUserCookie,
		                             CoreMask coreMask);

		/// Stops capturing packets (works will all type of startCapture*)
		void stopCapture();

		/// Opens a single RX channel (=RX queue) on this interface. All packets will be received on a single thread
		/// without core affinity. If the channel ID requested doesn't exist on this interface, the method will fail
		/// (return false)
		/// @param[in] channelId The requested channel ID
		/// @return True if this action succeeds, false otherwise
		bool openSingleRxChannel(uint8_t channelId);

		/// Opens a set of RX channels (=RX queues) on this interface, identified by their IDs. All packets will be
		/// received on a single thread without core affinity. If one of the channel IDs requested doesn't exist on this
		/// interface, the method will fail (return false)
		/// @param[in] channelIds An array of channel IDs
		/// @param[in] numOfChannelIds The channel ID array size
		/// @return True if this action succeeds, false otherwise
		bool openMultiRxChannels(const uint8_t* channelIds, int numOfChannelIds);

		/// Opens numOfRxChannelsToOpen RX channels. If numOfRxChannelsToOpen is larger than available RX queues for
		/// this interface than a number of RX channels will be opened on each RX queue. For example: if the user asks
		/// for 10 RX channels but the interface has only 4 RX queues, then 3 RX channels will be opened for RX-queue0
		/// and RX-queue2, and 2 RX channels will be opened for RX-queue2 and RX-queue3. Packets will be distributed
		/// between different RX queues on per-flow manner, but within multiple RX channels in the same RX queue packet
		/// will be distributed according to distribution requested by "dist"
		/// @param[in] numOfRxChannelsToOpen Number of RX channels to open
		/// @param[in] dist Distribution method
		/// @return True if this action succeeds, false otherwise
		bool openMultiRxChannels(uint8_t numOfRxChannelsToOpen, ChannelDistribution dist);

		/// Gets the number of RX channels currently open. RX channels aren't necessary interface's RX queues
		/// because in some cases the user asks to open several channels on the same queue. For example: if the user
		/// uses openMultiRxChannels() and asks to open 8 channels but interface has only 4 RX queues, 2 channels will
		/// be opened for each RX queue
		/// @return Number of opened RX channels
		uint8_t getNumOfOpenedRxChannels() const
		{
			return static_cast<uint8_t>(m_PfRingDescriptors.size());
		}

		/// Gets the total number of RX channels (RX queues) this interface has
		/// @return The number of RX channels (queues) for this interface
		uint8_t getTotalNumOfRxChannels() const;

		/// Gets the core used in the current thread context
		/// @return The system core used in the current thread context
		SystemCore getCurrentCoreId() const;

		/// Get the statistics of a specific thread/core (=RX channel)
		/// @param[in] core The requested core
		/// @param[out] stats A reference for the stats object where the stats are written. Current values will be
		/// overridden
		void getThreadStatistics(SystemCore core, PfRingStats& stats) const;

		/// Get the statistics of the current thread/core (=RX channel)
		/// @param[out] stats A reference for the stats object where the stats are written. Current values will be
		/// overridden
		void getCurrentThreadStatistics(PfRingStats& stats) const;

		/// Get the statistics for the entire device. If more than 1 RX channel is opened, this method aggregates the
		/// stats of all channels
		/// @param[out] stats A reference for the stats object where the stats are written. Current values will be
		/// overridden
		void getStatistics(PfRingStats& stats) const;

		/// Return true if filter is currently set
		/// @return True if filter is currently set, false otherwise
		bool isFilterCurrentlySet() const;

		/// Send a raw packet. This packet must be fully specified (the MAC address up)
		/// and it will be transmitted as-is without any further manipulation.
		/// This method doesn't change or manipulate the data in any way (hence the "const" declaration).
		/// Note this method flushes the TX queues after the data is sent. So if you want to send several packets
		/// In the burst please use sendPackets()
		/// @param[in] rawPacket The raw packet to send
		/// @return True if raw packet was sent completely, false otherwise
		bool sendPacket(const RawPacket& rawPacket);

		/// Send raw data. This data must be a valid and fully specified packet (the MAC address up);
		/// it will be transmitted as-is without any further manipulation.
		/// This method doesn't change or manipulate the data in any way (hence the "const" declaration).
		/// Note this method flushes the TX queues after the data is sent. So if you want to send several packets
		/// in the burst please use sendPackets()
		/// @param[in] packetData The raw data to send
		/// @param[in] packetDataLength the length of packetData
		/// @return True if raw packet was sent completely, false otherwise
		bool sendPacket(const uint8_t* packetData, int packetDataLength);

		/// Send a packet. This packet must be fully specified (the MAC address up)
		/// and it will be transmitted as-is without any further manipulation.
		/// This method doesn't change or manipulate the data in any way (hence the "const" declaration).
		/// Note this method flushes the TX queues after the data is sent. So if you want to send several packets
		/// In the burst please use sendPackets()
		/// @param[in] packet The packet to send
		/// @return True if raw packet was sent completely, false otherwise
		bool sendPacket(const Packet& packet);

		/// Send raw packets. All raw packets must be fully specified (the MAC address up)
		/// and it will be transmitted as-is without any further manipulation.
		/// This method doesn't change or manipulate the raw packets data in any way (hence the "const" declaration).
		/// This method flushes the TX queues only when the last packet is sent
		/// @param[in] rawPacketsArr The RawPacket array
		/// @param[in] arrLength RawPacket array length
		/// @return Number of packets that were sent completely
		int sendPackets(const RawPacket* rawPacketsArr, int arrLength);

		/// Send packets. All packets must be fully specified (the MAC address up)
		/// and it will be transmitted as-is without any further manipulation.
		/// This method doesn't change or manipulate the packets data in any way (hence the "const" declaration).
		/// This method flushes the TX queues only when the last packet is sent
		/// @param[in] packetsArr An array of pointers to Packet objects
		/// @param[in] arrLength Packet pointers array length
		/// @return Number of packets that were sent completely
		int sendPackets(const Packet** packetsArr, int arrLength);

		/// Send all raw packets pointed by the RawPacketVector. All packets must be fully specified (the MAC address
		/// up) and it will be transmitted as-is without any further manipulation. This method doesn't change or
		/// manipulate the packets data in any way (hence the "const" declaration). This method flushes the TX queues
		/// only when the last packet is sent
		/// @param[in] rawPackets The raw packet vector
		/// @return Number of raw packets that were sent completely
		int sendPackets(const RawPacketVector& rawPackets);

		// implement abstract methods

		/// Opens the entire device (including all RX channels/queues on this interface). All packets will be received
		/// on a single thread without core affinity
		/// @return True if this action succeeds, false otherwise
		bool open();

		/// Closes all RX channels currently opened in device
		void close();

		bool isOpened() const override
		{
			return m_DeviceOpened;
		}

	protected:
		bool doUpdateFilter(std::string const* filterAsString) override;

	private:
		bool removeFilterForAllChannels();
		bool setFilterForAllChannels(std::string const& filterAsString);
	};

}  // namespace pcpp

// GCOVR_EXCL_STOP
